package fr.umlv.proxir;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Properties;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import fr.umlv.proxir.cache.Cache;
import fr.umlv.proxir.filter.Filter;
import fr.umlv.proxir.httpmessages.AnswerFactory;
import fr.umlv.proxir.httpmessages.Request;
import fr.umlv.proxir.page.Page;
import fr.umlv.proxir.page.PageInRAM;

/**
 * This class represents the core of the proxy, it manages the connections and
 * make call to filters and cache
 * 
 * @author gourier & motais
 * 
 */
public class Proxir {
	private final static Logger LOGGER = Logger.getLogger("ProxIR");
	private final static int BUFFER_SIZE = 1024;
	private final static CharsetDecoder DECODER = Charset.forName("ISO-8859-1")
			.newDecoder();
	private final static CharsetEncoder ENCODER = Charset.forName("ISO-8859-1")
			.newEncoder();
	private final static Properties PROP = new Properties();

	private final ExecutorService executer = Executors.newCachedThreadPool();
	private final ServerSocketChannel ssc;
	private final MulticastSocket ms;

	// in the future we will have to set up the cache size
	private final static Cache cache = new Cache();
	private final static Filter filter = new Filter();

	/**
	 * initialize the proxy with the paramaters found in proxir.properties
	 * 
	 * @throws FileNotFoundException
	 *             if no properties is found
	 * @throws IOException
	 */
	public Proxir() throws FileNotFoundException, IOException {
		PROP.load(new FileInputStream("proxir.properties"));
		Handler fh = new FileHandler("logs/ProxIR.log%g", 65585, 5);
		Level l = Level.parse(PROP.getProperty("logLevel"));
		LOGGER.setLevel(l);
		fh.setLevel(l);
		fh.setFormatter(new SimpleFormatter());
		LOGGER.addHandler(fh);

		ssc = ServerSocketChannel.open();
		ssc.socket().bind(
				new InetSocketAddress(Integer.valueOf(PROP
						.getProperty("listeningPort"))));

		ms = new MulticastSocket(Integer.valueOf(PROP
				.getProperty("UDPListeningPort")));
		InetAddress add = InetAddress.getByName(PROP
				.getProperty("multicastAddress"));
		ms.joinGroup(add);
		ms.setSoTimeout(1);
		LOGGER.info("group " + add + " joined");
		initFilter();
	}

	/**
	 * starts the UDP multicast listener daemon
	 */
	private void startUDP() {
		new Thread(new Runnable() {

			@Override
			public void run() {

				while (!Thread.currentThread().isInterrupted()) {
					try {
						byte[] buffer = new byte[BUFFER_SIZE];
						DatagramPacket datag = new DatagramPacket(buffer,
								BUFFER_SIZE);
			

						ms.receive(datag);
						LOGGER.fine("other proxy " + datag.getAddress()
								+ " wants a page");
						answerUDPRequest(datag);
					} catch (IOException e) {
						// timeout, no trace, just try again
					}
				}

			}
		}).start();
	}

	/**
	 * this method handles UDP multicast request in another thread
	 * 
	 * @param datag
	 *            the datagram to handle
	 * @throws IOException
	 */
	private void answerUDPRequest(DatagramPacket datag) throws IOException {
		String stringRequest = new String(datag.getData(), 0, datag.getLength());
		if (stringRequest.startsWith("WHOHAS")) {
			String[] req = stringRequest.split(" ");
			if (cache.containsPage(req[1])) {
				LOGGER.info("sending YES "+req[1]+" to "+datag.getAddress());
				String answer = "YES " + req[1];
				datag.setLength(BUFFER_SIZE);
				datag.setData(answer.getBytes());
				ms.send(datag);
			}
		}
	}

	/**
	 * this method handles TCP requests in the current thread
	 * 
	 * @throws IOException
	 */
	private void startTCP() throws IOException {
		LOGGER.info("ProxIR server starting");
		while (!Thread.currentThread().isInterrupted()) {
			final SocketChannel sccp = ssc.accept();
			LOGGER.fine("Established TCP connexion from "
					+ ssc.socket().toString());
			executer.execute(new Runnable() {

				@Override
				public void run() {
					try {
						doRequest(sccp);
					} catch (IOException e) {
						LOGGER.severe(e.getMessage());
					}
				}
			});
		}
		executer.shutdown();

	}

	/**
	 * This method process the messages coming from the server. It caches pages
	 * which aren't yet in cache. Then it sends the request page to the client.
	 * 
	 * @param scps
	 *            SocketChannel between proxy and server
	 * @param sccp
	 *            SocketChannel between client and proxy
	 * @param pageID
	 *            the requested URL
	 * @return the runnable to start to process answers
	 * @throws IOException
	 */
	private static Runnable processAnswer(final SocketChannel scps,
			final SocketChannel sccp, final String pageID) throws IOException {
		return new Runnable() {

			@Override
			public void run() {
				try {
					Page p;
					LOGGER.info("Page requested is " + pageID);

					p = new PageInRAM();
					LOGGER.info("Page isn't in cache, caching now");

					cache.addPage(pageID, p);

					ByteBuffer bb = ByteBuffer.allocate(BUFFER_SIZE);
					while (scps.isOpen()
							&& !Thread.currentThread().isInterrupted()
							&& scps.read(bb) != -1) {
						bb.flip();
						byte[] content = new byte[bb.remaining()];
						bb.get(content, 0, content.length);
						p.addBuffer(content);
						
						String contentType = p.getContentType();
						String contentEncoding = p.getContentEncoding();
		
						if (contentType != null
								&& contentType.equals("text/html")
								&& contentEncoding == null) {

							String charsetName = p.getCharset();
							String tmp;

							if (charsetName != null) {
								tmp = new String(content, Charset
										.forName(charsetName));
							} else {
								tmp = new String(content);
							}

							if (filter.isSentenceAllowed(tmp)) {
								LOGGER.fine("Sentence allowed");
								sccp.write(ByteBuffer.wrap(content));
							} else {
								LOGGER.fine("Sentence forbidden");
								Page p2 = cache.getPage("Forbidden");
								for (byte[] buff : p2.getContent()) {
									sccp.write(ByteBuffer.wrap(buff));
								}
								cache.remove(p);
							}
						} else {
							sccp.write(ByteBuffer.wrap(content));
						}
						bb.clear();
					}

					if (Thread.currentThread().isInterrupted()) {
						scps.close();
						sccp.close();
					}
				} catch (AsynchronousCloseException e) {
					LOGGER.fine(e.getMessage());
				} catch (IOException e) {
					LOGGER.severe(e.getMessage());
				} finally {
					try {
						scps.socket().getInputStream().close();
					} catch (IOException e) {
					}
				}
			}
		};
	}

	/**
	 * this method process the request and the messages from the client to the
	 * server it first verifies if the requested pages are in local cache then
	 * it verifies if other proxies have the page in cache if not then it'll
	 * connect to the server and establish a relay between the client and server
	 * 
	 * @param sccp
	 *            SocketChannel between client and proxy
	 * @throws IOException
	 */
	private void doRequest(final SocketChannel sccp) throws IOException {
		SocketChannel scps;
		scps = SocketChannel.open();

		ByteBuffer bb = ByteBuffer.allocate(BUFFER_SIZE);
		CharBuffer cb = CharBuffer.allocate(BUFFER_SIZE);
		ByteBuffer bbout = ByteBuffer.allocate(BUFFER_SIZE);
		CharBuffer cbout = CharBuffer.allocate(BUFFER_SIZE);
		String oldHost = "";
		String s = "";
		while (sccp.read(bb) != -1) {
			bb.flip();
			DECODER.decode(bb, cb, true);
			bb.compact();
			cb.flip();
			while (cb.hasRemaining() && !s.endsWith("\n\n")
					&& !s.endsWith("\r\n\r\n")) {
				s += cb.get();
			}
			Request request = getRequest(s);

			if (filter.isPageAllowed(request.getHost())
					&& filter.isPortAllowed(request.getPort())) {

				if (request.getMethod().equals("POST")) {
					LOGGER.finest("\n\t\t\tRequete POST : " + s);
					StringBuilder sb = new StringBuilder();
					sb.append(s);
					String postData = "";
					while (cb.hasRemaining() && !postData.endsWith("\n\n")
							&& !postData.endsWith("\r\n\r\n")) {
						postData += cb.get();
					}
					s = sb.toString() + postData;
					request = getRequest(s);
				}

				cb.compact();
				if (s.endsWith("\n\n") || s.endsWith("\r\n\r\n")) {
					// OK THAT'S THE REQUEST, DO I HAVE IT ?
					if (cache.containsPage(request.getPageRequested())) {
						Page p = cache.getPage(request.getPageRequested());
						LOGGER.info("Request page is in cache, sending to client");
						LOGGER.finest("page \n" + p);
						for (byte[] buff : p.getContent()) {
							sccp.write(ByteBuffer.wrap(buff));
						}
					} else {
						// OK NOT IN MY CACHE,
						// DO OTHERS HAVE IT ?
						byte[] buf = new byte[BUFFER_SIZE];
						InetAddress add = InetAddress.getByName(PROP
								.getProperty("multicastAddress"));
						int port = Integer.valueOf(PROP
								.getProperty("UDPListeningPort"));
						DatagramPacket dp = new DatagramPacket(buf,
								BUFFER_SIZE, add, port);
						dp.setData(("WHOHAS " + request.getPageRequested())
								.getBytes());
						ms.send(dp);
						buf = new byte[BUFFER_SIZE];
						dp.setData(buf);
						while (true) {
							try {
								ms.setSoTimeout(1);
								ms.receive(dp);
								String res = new String(buf);
								if (res.startsWith("YES "
										+ request.getPageRequested())) {
									LOGGER
											.fine("another proxy has the requested page");
									scps.close();
									scps = SocketChannel.open();
									scps.connect(new InetSocketAddress(dp
											.getAddress(), Integer.valueOf(PROP
											.getProperty("listeningPort"))));
									executer.execute(processAnswer(scps, sccp,
											request.getPageRequested()));
									break;
								}
							} catch (SocketTimeoutException ste) {
								LOGGER.info("TIME OUT");
								LOGGER.fine("Host is " + request.getHost()
										+ "\t\t Previous host was " + oldHost);
								if (!scps.isConnected()
										|| oldHost != request.getHost()) {
									LOGGER.info("Connecting to "
											+ request.getHost());
									scps.close();
									scps = SocketChannel.open();
									scps.connect(new InetSocketAddress(request
											.getHost(), request.getPort()));
									executer.execute(processAnswer(scps, sccp,
											request.getPageRequested()));
									oldHost = request.getHost();
								}
								break;
							} catch (IOException e) {
								// either connexion error
								// try again
							}
						}

						LOGGER.fine("Sending request to "
								+ scps.socket().getInetAddress() + "\n" + s
								+ "\n");
						cbout = CharBuffer.wrap(s);
						s = "";
						// we didn't read the '\n' before
						while (cbout.hasRemaining()) {
							ENCODER.encode(cbout, bbout, true);
							bbout.flip();
							scps.write(bbout);
							bbout.clear();
						}
					}
				}
			} else {
				executer.execute(processAnswer(scps, sccp, "Forbidden"));
				scps.close();
			}

		}
	}

	/**
	 * debugging methods used to print a bytebuffer
	 * @param bb the bytebuffer to print
	 */
	public static void printByteBuffer(ByteBuffer bb) {
		CharBuffer cbout = CharBuffer.allocate(BUFFER_SIZE);
		DECODER.decode(bb, cbout, true);
		cbout.flip();
		bb.rewind();
		LOGGER.finest("ByteBuffer:\n" + cbout.toString());
	}

	/**
	 * parses the request with \s.
	 * @param request the request to parse
	 * @return a table with each element is a token of the request
	 */
	private static Request getRequest(String request) {
		LOGGER.finest("Requete:\n" + request);
		String host = "";
		Scanner sc = new Scanner(request);
		String line = sc.nextLine();

		// tab[0]=METHOD tab[1]=URL tab[2]=VERSION
		String[] splittedHTTPRequest = line.split(" ");
		Scanner sc2 = new Scanner(line);

		sc2.next(); // GET,HEAD,OPTIONS etc...
		host = splittedHTTPRequest[1];// host as found after GET
		Request req = new Request(host, 80, splittedHTTPRequest);
		while (sc.hasNextLine()) {
			line = sc.nextLine();
			if (line.contains("Host:")) {
				sc2 = new Scanner(line);
				sc2.next();
				host = sc2.next();

				if (host.contains(":")) {
					String[] hostPort = host.split(":");
					req.setHost(hostPort[0]);
					req.setPort(Integer.parseInt(hostPort[1]));
				} else {
					req.setHost(host);
				}
			}

		}
		return req;
	}

	/**
	 * inits the filter
	 */
	private static void initFilter() {
		cache.addPage("Forbidden", AnswerFactory
				.buildForbiddenAnswer("Stop DL Pr0n  OGC"));
		filter.addAllForbiddenPage(PROP.get("forbiddenPages").toString().split(
				","));
		filter.addAllForbiddenPort(PROP.get("forbiddenPorts").toString().split(
				","));
		filter.addAllForbiddenWord(PROP.get("forbiddenWords").toString().split(
				","));
	}

	/**
	 * starts the proxy server
	 * @throws IOException
	 */
	public void start() throws IOException {
		this.startUDP();
		this.startTCP();
	}

}
